/*
 * Bytecode analysis framework
 * Copyright (C) 2005, University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba.interproc;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.annotation.WillClose;

import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.classfile.FieldOrMethodDescriptor;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.util.Util;

/**
 * Property database for interprocedural analysis.
 * 
 * @param <KeyType>   key type: either MethodDescriptor or FieldDescriptor
 * @param <ValueType> value type: a value that summarizes some property of the associated key
 * @author David Hovemeyer
 */
public abstract class PropertyDatabase<KeyType extends FieldOrMethodDescriptor, ValueType> {
	private Map<KeyType, ValueType> propertyMap;

	/**
	 * Constructor.
	 * Creates an empty property database. 
	 */
	protected PropertyDatabase() {
		this.propertyMap = new HashMap<KeyType, ValueType>();
	}

	/**
	 * Set a property.
	 * 
	 * @param key   the key
	 * @param property the property
	 */
	public void setProperty(KeyType key, ValueType property) {
		propertyMap.put(key, property);
	}

	/**
	 * Get a property.
	 * 
	 * @param key the key
	 * @return the property, or null if no property is set for this key
	 */
	public ValueType getProperty(KeyType key) {
		return propertyMap.get(key);
	}

	public Set<KeyType> getKeys() {
		return propertyMap.keySet();
	}
	public Collection<Map.Entry<KeyType, ValueType>> entrySet() {
		return propertyMap.entrySet();
	}
	/**
	 * Return whether or not the database is empty.
	 * 
	 * @return true if the database is empty, false it it has at least one entry
	 */
	public boolean isEmpty() {
		return propertyMap.isEmpty();
	}

	/**
	 * Remove a property.
	 * 
	 * @param key the key
	 * @return the old property, or null if there was no property defined for this key
	 */
	public ValueType removeProperty(KeyType key) {
		return propertyMap.remove(key);
	}

	/**
	 * Read property database from given file.
	 * 
	 * @param fileName name of the database file
	 * @throws IOException
	 * @throws MethodPropertyDatabaseFormatException
	 */
	public void readFromFile(String fileName) throws IOException, PropertyDatabaseFormatException {
		read(new FileInputStream(fileName));
	}

	/**
	 * Read property database from an input stream.
	 * The InputStream is guaranteed to be closed, even if an
	 * exception is thrown.
	 * 
	 * @param in the InputStream
	 * @throws IOException
	 * @throws MethodPropertyDatabaseFormatException
	 */
	public void read(@WillClose InputStream in) throws IOException, PropertyDatabaseFormatException {
		BufferedReader reader = null;

		try {
			reader = new BufferedReader(
					Util.getReader(in));
			String line;
			while ((line = reader.readLine()) != null) {
				line = line.trim();
				if (line.equals(""))
					continue;
				int bar = line.indexOf('|');
				if (bar < 0) {
					throw new PropertyDatabaseFormatException(
					"Invalid property database: missing separator");
				}
				KeyType key = parseKey(line.substring(0, bar));
				ValueType property = decodeProperty(line.substring(bar+1));

				setProperty(key, property);
			}
		} finally {
			try {
				if (reader != null)
					reader.close();
			} catch (IOException e) {
				// Ignore
			}
		}
	}

	/**
	 * Write property database to given file.
	 * 
	 * @param fileName name of the database file
	 * @throws IOException
	 */
	public void writeToFile(String fileName) throws IOException {
		write(new FileOutputStream(fileName));
	}

//	@SuppressWarnings("unchecked")
//	private KeyType intern(XFactory xFactory, KeyType key) {
//		KeyType result = key;
//		try {
//			if (key instanceof XField)
//				return (KeyType)  xFactory.intern((XField)key);
//			else if (key instanceof XMethod) 
//				return (KeyType)  xFactory.intern((XMethod)key);
//		} catch (Exception e) {
//			return key;
//		}
//		return result;
//	}
	
	/**
	 * Write property database to an OutputStream.
	 * The OutputStream is guaranteed to be closed, even if an
	 * exception is thrown.
	 * 
	 * @param out the OutputStream
	 * @throws IOException
	 */
	public void write(@WillClose OutputStream out) throws IOException {
		BufferedWriter writer = null;
		boolean missingClassWarningsSuppressed = AnalysisContext.currentAnalysisContext().setMissingClassWarningsSuppressed(true);

		try {
			writer = new BufferedWriter(
					new OutputStreamWriter(out, Charset.forName("UTF-8")));

			TreeSet<KeyType> sortedMethodSet = new TreeSet<KeyType>();
			sortedMethodSet.addAll(propertyMap.keySet());
			for (KeyType key : sortedMethodSet) {
				if (AnalysisContext.currentAnalysisContext().isApplicationClass(key.getClassDescriptor())) {

					ValueType property = propertyMap.get(key);
					
					writeKey(writer, key);
					writer.write("|");
					writer.write(encodeProperty(property));
					writer.write("\n");
				}
			}
		} finally {
			AnalysisContext.currentAnalysisContext().setMissingClassWarningsSuppressed(missingClassWarningsSuppressed);

			try {
				if (writer != null)
					writer.close();
			} catch (IOException e) {
				// Ignore
			}
		}
	}

	/**
	 * Parse a key from a String.
	 * 
	 * @param s a String
	 * @return the decoded key
	 * @throws PropertyDatabaseFormatException
	 */
	protected abstract KeyType parseKey(String s) throws PropertyDatabaseFormatException;

	/**
	 * Write an encoded key to given Writer.
	 * 
	 * @param writer the Writer
	 * @param key    the key
	 */
	protected abstract void writeKey(Writer writer, KeyType key) throws IOException;

	/**
	 * Subclasses must define this to instantiate the actual property
	 * value from its string encoding.
	 * 
	 * @param propStr String containing the encoded property
	 * @return the property
	 * @throws MethodPropertyDatabaseFormatException
	 */
	protected abstract ValueType decodeProperty(String propStr)
	throws PropertyDatabaseFormatException;

	/**
	 * Subclasses must define this to encode a property
	 * as a string for output to a file.
	 * 
	 * @param property the property
	 * @return a String which encodes the property
	 */
	protected abstract String encodeProperty(ValueType property);

}
